﻿using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text;
using Camstar.Utils.csv.Data;

namespace Camstar.Utils.csv
{
    /// <summary>
    /// A class for reading a Character Separated Value stream.
    /// </summary>
    /// <remarks>
    /// The most common stream is a CSV (Comma Separated Value) file, see: <see cref="http://en.wikipedia.org/wiki/Comma-separated_values" />
    /// </remarks>
    public class CsvReader : CsvBase
    {
        #region Constructors

        /// <summary>
        /// Base constructor for this instance.
        /// </summary>
        /// <param name="reader">The text reader to be read.</param>
        /// <param name="isLocal">A value indicating if the text reader was created by this instance.</param>
        protected CsvReader(
            TextReader reader,
            bool isLocal)
        {
            isLocalReader = isLocal;
            BaseReader = reader;
        }

        /// <summary>
        /// Initializes a new instance the CsvReader class for the specified text reader.
        /// </summary>
        /// <param name="reader">The text reader to be read.</param>
        public CsvReader(
            TextReader reader)
            : this(reader, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of the CsvReader class for the specified
        /// stream.
        /// </summary>
        /// <param name="stream">The stream to be read.</param>
        /// <exception cref="System.ArgumentException">The stream does not support reading.</exception>
        /// <exception cref="System.ArgumentNullException">stream is null.</exception>
        public CsvReader(
            Stream stream)
            : this(new StreamReader(stream), true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the CsvReader class for the specified
        /// file name with the specified byte order mark detection
        /// option.
        /// </summary>
        /// <param name="path">The complete file path to be read.</param>
        /// <exception cref="System.ArgumentException">path is an empty string ("").</exception>
        /// <exception cref="System.ArgumentNullException">path is null.</exception>
        /// <exception cref="System.IO.FileNotFoundException">The file cannot be found.</exception>
        /// <exception cref="System.IO.DirectoryNotFoundException">The specified path is invalid, such as being on an unmapped drive.</exception>
        /// <exception cref="System.NotSupportedException">path includes an incorrect or invalid syntax for file name, directory name, or volume label.</exception>
        public CsvReader(
            string path)
            : this(new StreamReader(path), true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the CsvReader class for the specified
        /// stream, with the specified byte order mark detection option.
        /// </summary>
        /// <param name="stream">The stream to be read.</param>
        /// <param name="detectEncodingFromByteOrderMarks">Indicates whether to look for byte order marks at the beginning of the file.</param>
        /// <exception cref="System.ArgumentException">The stream does not support reading.</exception>
        /// <exception cref="System.ArgumentNullException">stream is null.</exception>
        public CsvReader(
            Stream stream,
            bool detectEncodingFromByteOrderMarks)
            : this(new StreamReader(stream, detectEncodingFromByteOrderMarks), true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the CsvReader class for the specified
        /// stream, with the specified character encoding.
        /// </summary>
        /// <param name="stream">The stream to be read.</param>
        /// <param name="encoding">The character encoding to use.</param>
        /// <exception cref="System.ArgumentException">The stream does not support reading.</exception>
        /// <exception cref="System.ArgumentNullException">stream or encoding is null.</exception>
        public CsvReader(
            Stream stream,
            Encoding encoding)
            : this(new StreamReader(stream, encoding), true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the CsvReader class for the specified
        /// file name with the specified byte order mark detection option.
        /// </summary>
        /// <param name="path">The complete file path to be read.</param>
        /// <param name="detectEncodingFromByteOrderMarks">Indicates whether to look for byte order marks at the beginning of the file.</param>
        /// <exception cref="System.ArgumentException">path is an empty string ("").</exception>
        /// <exception cref="System.ArgumentNullException">path is null.</exception>
        /// <exception cref="System.IO.FileNotFoundException">The file cannot be found.</exception>
        /// <exception cref="System.IO.DirectoryNotFoundException">The specified path is invalid, such as being on an unmapped drive.</exception>
        /// <exception cref="System.NotSupportedException">path includes an incorrect or invalid syntax for file name, directory name, or volume label.</exception>
        public CsvReader(
            string path,
            bool detectEncodingFromByteOrderMarks)
            : this(new StreamReader(path, detectEncodingFromByteOrderMarks), true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the CsvReader class for the specified
        /// file name with the specified character encoding.
        /// </summary>
        /// <param name="path">The complete file path to be read.</param>
        /// <param name="encoding">The character encoding to use.</param>
        /// <exception cref="System.ArgumentException">path is an empty string ("").</exception>
        /// <exception cref="System.ArgumentNullException">path or encoding is null.</exception>
        /// <exception cref="System.IO.FileNotFoundException">The file cannot be found.</exception>
        /// <exception cref="System.IO.DirectoryNotFoundException">The specified path is invalid, such as being on an unmapped drive.</exception>
        /// <exception cref="System.NotSupportedException">path includes an incorrect or invalid syntax for file name, directory name, or volume label.</exception>
        public CsvReader(
            string path,
            Encoding encoding)
            : this(new StreamReader(path, encoding), true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the CsvReader class for the specified
        /// stream, with the specified character encoding, and byte order mark detection
        /// option.
        /// </summary>
        /// <param name="stream">The stream to be read.</param>
        /// <param name="encoding">The character encoding to use.</param>
        /// <param name="detectEncodingFromByteOrderMarks">Indicates whether to look for byte order marks at the beginning of the file.</param>
        /// <exception cref="System.ArgumentException">The stream does not support reading.</exception>
        /// <exception cref="System.ArgumentNullException">stream or encoding is null.</exception>
        public CsvReader(
            Stream stream,
            Encoding encoding,
            bool detectEncodingFromByteOrderMarks)
            : this(new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks), true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the CsvReader class for the specified
        /// file name, with the specified character encoding, and byte order mark detection
        /// option.
        /// </summary>
        /// <param name="path">The complete file path to be read.</param>
        /// <param name="encoding">The character encoding to use.</param>
        /// <param name="detectEncodingFromByteOrderMarks">Indicates whether to look for byte order marks at the beginning of the file.</param>
        /// <exception cref="System.ArgumentException">path is an empty string ("").</exception>
        /// <exception cref="System.ArgumentNullException">path or encoding is null.</exception>
        /// <exception cref="System.IO.FileNotFoundException">The file cannot be found.</exception>
        /// <exception cref="System.IO.DirectoryNotFoundException">The specified path is invalid, such as being on an unmapped drive.</exception>
        /// <exception cref="System.NotSupportedException">path includes an incorrect or invalid syntax for file name, directory name, or volume label.</exception>
        public CsvReader(
            string path,
            Encoding encoding,
            bool detectEncodingFromByteOrderMarks)
            : this(new StreamReader(path, encoding, detectEncodingFromByteOrderMarks), true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the CsvReader class for the specified
        /// stream, with the specified character encoding, byte order mark detection
        /// option, and buffer size.
        /// </summary>
        /// <param name="stream">The stream to be read.</param>
        /// <param name="encoding">The character encoding to use.</param>
        /// <param name="detectEncodingFromByteOrderMarks">Indicates whether to look for byte order marks at the beginning of the file.</param>
        /// <param name="bufferSize">The minimum buffer size, in number of 16-bit characters.</param>
        /// <exception cref="System.ArgumentException">The stream does not support reading.</exception>
        /// <exception cref="System.ArgumentNullException">stream or encoding is null.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">buffersize less than or equal to zero.</exception>
        public CsvReader(
            Stream stream,
            Encoding encoding,
            bool detectEncodingFromByteOrderMarks,
            int bufferSize)
            : this(new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize), true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the CsvReader class for the specified
        /// file name, with the specified character encoding, byte order mark detection
        /// option, and buffer size.
        /// </summary>
        /// <param name="path">The complete file path to be read.</param>
        /// <param name="encoding">The character encoding to use.</param>
        /// <param name="detectEncodingFromByteOrderMarks">Indicates whether to look for byte order marks at the beginning of the file.</param>
        /// <param name="bufferSize">The minimum buffer size, in number of 16-bit characters.</param>
        /// <exception cref="System.ArgumentException">path is an empty string ("").</exception>
        /// <exception cref="System.ArgumentNullException">path or encoding is null.</exception>
        /// <exception cref="System.IO.FileNotFoundException">The file cannot be found.</exception>
        /// <exception cref="System.IO.DirectoryNotFoundException">The specified path is invalid, such as being on an unmapped drive.</exception>
        /// <exception cref="System.NotSupportedException">path includes an incorrect or invalid syntax for file name, directory name, or volume label.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">buffersize less than or equal to zero.</exception>
        public CsvReader(
            string path,
            Encoding encoding,
            bool detectEncodingFromByteOrderMarks,
            int bufferSize)
            : this(new StreamReader(path, encoding, detectEncodingFromByteOrderMarks, bufferSize), true)
        {
        }

        #endregion // Constructors

        #region Private data

        /// <summary>
        /// The string builder used to accumulate column values.
        /// </summary>
        private readonly StringBuilder builder = new StringBuilder(1024);

        /// <summary>
        /// A value indicating if the underlying text reader is local.
        /// </summary>
        private readonly bool isLocalReader;

        /// <summary>
        /// A value indicating if the end of the stream has been reached.
        /// </summary>
        private bool isEndOfStream;

        /// <summary>
        /// If a column value begins with equals, discard that character (for Excel formulas).
        /// </summary>
        private bool isEquals;

        /// <summary>
        /// A value indicating if this is the first character in the line.
        /// </summary>
        private bool isFirstCharInLine;

        /// <summary>
        /// Ignore the CR character if it immediately precedes the end of row character.
        /// </summary>
        private bool isIgnoreCr;

        /// <summary>
        /// Trim white space from the start and end of unquoted column values.
        /// </summary>
        private bool isTrim;

        /// <summary>
        /// The name space for this reader (used when generating column names).
        /// </summary>
        private INameSpace nameSpace;

        #endregion // Private data

        #region Properties

        /// <summary>
        /// Gets the underlying text reader for this instance.
        /// </summary>
        public TextReader BaseReader { get; private set; }

        /// <summary>
        /// Gets or sets the initial capacity for ReadRow lists.
        /// </summary>
        public int Capacity { get; set; }

        /// <summary>
        /// Get a value indicating if more columns remain.
        /// </summary>
        public bool HasColumn
        {
            get { return isInRow; }
        }

        /// <summary>
        /// Get a value indicating if more rows remain.
        /// </summary>
        public bool HasRow
        {
            get { return isEndOfStream == false; }
        }

        /// <summary>
        /// Gets or sets the name space used while generating columns.
        /// </summary>
        public INameSpace NameSpace
        {
            get
            {
                // If no name space, create one
                if (nameSpace == null)
                    nameSpace = new SimpleNameSpace();

                // Return the name space
                return nameSpace;
            }
            set { nameSpace = value; }
        }

        #endregion // Properties

        #region Public methods

        #region Public methods (columns)

        /// <summary>
        /// Read a column of the specified type.
        /// </summary>
        /// <typeparam name="T">The type of column to read.</typeparam>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.ArgumentException">type is not supported.</exception>
        /// <exception cref="System.FormatException">column is not a valid format.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range.</exception>
        public T ReadColumn<T>()
        {
            return (T) ReadColumn(typeof (T));
        }

        /// <summary>
        /// Read a column of the specified type.
        /// </summary>
        /// <param name="type">The type of column to read.</param>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.ArgumentException">type is not supported.</exception>
        /// <exception cref="System.FormatException">column is not a valid format.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range.</exception>
        public object ReadColumn(
            Type type)
        {
            // Dispatch to read the correct type
            if (type == typeof (string)) return ReadString();
            if (type == typeof (int)) return ReadInt32();
            if (type == typeof (uint)) return ReadUInt32();
            if (type == typeof (long)) return ReadInt64();
            if (type == typeof (ulong)) return ReadUInt64();
            if (type == typeof (decimal)) return ReadDecimal();
            if (type == typeof (double)) return ReadDouble();
            if (type == typeof (float)) return ReadSingle();
            if (type == typeof (short)) return ReadInt16();
            if (type == typeof (ushort)) return ReadUInt16();
            if (type == typeof (DateTime)) return ReadDateTime();
            if (type == typeof (byte)) return ReadByte();
            if (type == typeof (sbyte)) return ReadSByte();
            if (type == typeof (bool)) return ReadBoolean();
            if (type == typeof (char)) return ReadChar();

            // Indicate that we do not support the type
            throw new ArgumentException("type is not supported.");
        }

        /// <summary>
        /// Read a column containing a boolean value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for a boolean.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        public bool ReadBoolean()
        {
            return bool.Parse(ReadString());
        }

        /// <summary>
        /// Read a column containing an unsigned byte value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for an unsigned byte.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range for an unsigned byte.</exception>
        public byte ReadByte()
        {
            return byte.Parse(ReadString());
        }

        /// <summary>
        /// Read a column containing a char value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for a char.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        public char ReadChar()
        {
            return char.Parse(ReadString());
        }

        /// <summary>
        /// Read a column containing a date/time value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for a date/time.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        public DateTime ReadDateTime()
        {
            return DateTime.Parse(ReadString());
        }

        /// <summary>
        /// Read a column containing a decimal value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for a decimal.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range for a decimal.</exception>
        public decimal ReadDecimal()
        {
            return decimal.Parse(ReadString());
        }

        /// <summary>
        /// Read a column containing a double-precision floating point value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for a double-precision floating point value.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range for a double-precision floating point value.</exception>
        public double ReadDouble()
        {
            return double.Parse(ReadString());
        }

        /// <summary>
        /// Read a column containing a signed, 16-bit integral value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for a signed, 16-bit integral value.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range for a signed, 16-bit integral value.</exception>
        public short ReadInt16()
        {
            return short.Parse(ReadString());
        }

        /// <summary>
        /// Read a column containing a signed, 32-bit integral value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for a signed, 32-bit integral value.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range for a signed, 32-bit integral value.</exception>
        public int ReadInt32()
        {
            return int.Parse(ReadString());
        }

        /// <summary>
        /// Read a column containing a signed, 64-bit integral value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for a signed, 64-bit integral value.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range for a signed, 64-bit integral value.</exception>
        public long ReadInt64()
        {
            return long.Parse(ReadString());
        }

        /// <summary>
        /// Read a column containing a signed byte value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for a signed byte.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range for a signed byte.</exception>
        public sbyte ReadSByte()
        {
            return sbyte.Parse(ReadString());
        }

        /// <summary>
        /// Read a column containing a single-precision floating point value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for a single-precision floating point value.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range for a single-precision floating point value.</exception>
        public float ReadSingle()
        {
            return float.Parse(ReadString());
        }

        /// <summary>
        /// Read a column.
        /// </summary>
        /// <returns></returns>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        public string ReadString()
        {
            return ReadString(false);
        }

        /// <summary>
        /// Read a column.
        /// </summary>
        /// <param name="skip">A value indicating if the column should be skipped.</param>
        /// <returns>The column value (or null if skipping columns).</returns>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        public string ReadString(
            bool skip)
        {
            // If we are not in a row, throw an exception
            if (!isInRow)
                throw new InvalidOperationException("CSV reader is not in a row.");

            // Prepare for the loop
            bool isFirstCharInColumn = true;
            bool inComment = false;
            int quoteChar = -1;
            int length = 0;
            int next = 0;

            // Zero out the string builder
            builder.Length = 0;

            // Loop for all characters in column...
            while (next >= 0)
            {
                // Read the next character
                next = BaseReader.Read();
                if (next < 0)
                    break;

                // Get the next character
                var nextChar = (char) next;

                // If we are in a comment...
                if (inComment)
                {
                    // If end of row, stop
                    if (isEndRow(nextChar))
                        break;

                    // Ignore the character
                    continue;
                } // If we are in a comment...

                // If this is the first character...
                if (isFirstCharInColumn)
                {
                    // If first character in line...
                    if (isFirstCharInLine)
                    {
                        // If checking for empty lines...
                        if (isEmptyLine)
                        {
                            // If end of row...
                            if (isEndRow(nextChar))
                            {
                                // Re-enter row and continue
                                isInRow = true;
                                continue;
                            } // If end of row...
                        } // If checking for empty lines...

                        // No longer first character in line
                        isFirstCharInLine = false;
                    } // If first character in line...

                    // If end of row character, stop
                    if (isEndRow(nextChar))
                        break;

                    // If starting a comment...
                    if (isComment && nextChar == CommentChar)
                    {
                        // Enter a comment and continue
                        inComment = true;
                        continue;
                    } // If starting a comment...

                    // If we are trimming leading white space, do so
                    if (isTrim && nextChar != EndColumnChar && Char.IsWhiteSpace(nextChar))
                        continue;

                    // Do not come this way again
                    isFirstCharInColumn = false;

                    // If skipping equals (for Excel formulas), do so
                    if (isEquals && nextChar == '=')
                        continue;

                    // If entering double quoted string...
                    if (isQuote && nextChar == '"')
                    {
                        // Enter the quote
                        quoteChar = nextChar;
                        continue;
                    } // If entering single quoted string...

                    // If entering single quoted string...
                    if (isSingleQuote && nextChar == '\'')
                    {
                        // Enter the quote
                        quoteChar = nextChar;
                        continue;
                    } // If entering single quoted string...
                } // If this is the first character...
                else
                {
                    // ELSE: If this is the first character...
                    // If we are in a quoted value...
                    if (quoteChar >= 0)
                    {
                        // If starting a comment...
                        if (isCommentQuote && nextChar == CommentChar)
                        {
                            // Start the comment
                            isComment = true;
                            continue;
                        } // If starting a comment...

                        // If this character is a quote...
                        if (nextChar == quoteChar)
                        {
                            // Peek at the next character
                            next = BaseReader.Peek();
                            if (next < 0)
                                break;

                            // If not an escaped quote...
                            if (!isDouble || next != quoteChar)
                            {
                                // End quoting
                                quoteChar = -1;
                                continue;
                            } // If not an escaped quote...

                            // Consume the next character
                            BaseReader.Read();
                        } // If this character is a quote...
                        else
                        {
                            // ELSE: If this character is a quote...
                            // If this character is an escape sequence...
                            if (isEscape && nextChar == EscapeChar)
                            {
                                // Read the next character
                                next = BaseReader.Read();

                                // If not last, use the next character
                                if (next >= 0)
                                    nextChar = (char) next;
                            } // If this character is an escape sequence...
                        } // ELSE: If this character is a quote...

                        // If not skipping characters, add the character
                        if (!skip)
                            builder.Append(nextChar);

                        // Continue to next character
                        continue;
                    } // If we are in a quoted value...
                } // ELSE: If this is the first character...

                // If free escape character...
                if (isFreeEscape && nextChar == EscapeChar)
                {
                    // Get the next character
                    next = BaseReader.Read();

                    // If next character starts a comment...
                    if (!isCommentQuote && next == CommentChar)
                    {
                        // Start a comment
                        inComment = true;
                        continue;
                    } // If next character starts a comment...

                    // If not last, use the next character
                    if (next >= 0)
                        nextChar = (char) next;
                }
                else
                {
                    // ELSE: If free escape character...
                    // If next character starts a comment...
                    if (isCommentInLine && nextChar == CommentChar)
                    {
                        // Start a comment
                        inComment = true;
                        continue;
                    } // If next character starts a comment...

                    // If end of column, we are done with column
                    if (nextChar == EndColumnChar)
                        break;

                    // If end of row, stop
                    if (isEndRow(nextChar))
                        break;
                } // ELSE: If free escape character...

                // If skipping characters, just continue
                if (skip)
                    continue;

                // Save the character
                builder.Append(nextChar);

                // If trimming characters, remember last non-white space
                if (isTrim && !Char.IsWhiteSpace(nextChar))
                    length = builder.Length;
            } // Loop for all characters in column...

            // If end of stream encountered...
            if (next < 0)
            {
                // Remember the end of the stream
                isEndOfStream = true;
                isInRow = false;
            } // If end of stream encountered...

            // If skipping column, just return
            if (skip)
                return null;

            // If trimming, trim trailing white space
            if (isTrim)
                builder.Length = length;

            // Return the result
            return builder.ToString();
        }

        /// <summary>
        /// Read a column containing an unsigned, 16-bit integral value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for an unsigned, 16-bit integral value.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range for an unsigned, 16-bit integral value.</exception>
        public ushort ReadUInt16()
        {
            return ushort.Parse(ReadString());
        }

        /// <summary>
        /// Read a column containing an unsigned, 32-bit integral value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for an unsigned, 32-bit integral value.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range for an unsigned, 32-bit integral value.</exception>
        public uint ReadUInt32()
        {
            return uint.Parse(ReadString());
        }

        /// <summary>
        /// Read a column containing an unsigned, 64-bit integral value.
        /// </summary>
        /// <returns>The value of the column.</returns>
        /// <exception cref="System.FormatException">column is not a valid format for an unsigned, 64-bit integral value.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range for an unsigned, 64-bit integral value.</exception>
        public ulong ReadUInt64()
        {
            return ulong.Parse(ReadString());
        }

        #endregion // Public methods (columns)

        #region Public methods (rows)

        /// <summary>
        /// Read a row into a list.
        /// </summary>
        /// <typeparam name="T">The type of columns to read.</typeparam>
        /// <returns>The list of columns (or null).</returns>
        /// <exception cref="System.ArgumentException">type is not supported.</exception>
        /// <exception cref="System.FormatException">column is not a valid format.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range.</exception>
        public List<T> ReadRow<T>()
        {
            // If we can not start a row, return null
            if (!ReadRowStart())
                return null;

            // Create a list to receive the columns
            var list = new List<T>(Capacity);

            // Loop to read the columns...
            while (isInRow)
                list.Add(ReadColumn<T>());

            // If length is greater than capacity, increase capacity
            if (list.Count > Capacity)
                Capacity = list.Count;

            // Return the list
            return list;
        }

        /// <summary>
        /// Read a row into a list.
        /// </summary>
        /// <returns>The list of columns (or null).</returns>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        public List<string> ReadRow()
        {
            // If we can not start a row, return null
            if (!ReadRowStart())
                return null;

            // Create a list to receive the columns
            var list = new List<string>(Capacity);

            // Loop to read the columns...
            while (isInRow)
                list.Add(ReadString());

            // If length is greater than capacity, increase capacity
            if (list.Count > Capacity)
                Capacity = list.Count;

            // Return the list
            return list;
        }

        /// <summary>
        /// Read a row for a data table.
        /// </summary>
        /// <param name="table">The data table to receive the row.</param>
        /// <param name="addToTable">A value indicating if the row is added to the table.</param>
        /// <returns>The row which was read (or null).</returns>
        /// <exception cref="System.ArgumentException">type is not supported.</exception>
        /// <exception cref="System.Data.ConstraintException">The addition invalidates a constraint.</exception>
        /// <exception cref="System.Data.NoNullAllowedException">The addition tries to put a null in a System.Data.DataColumn where System.Data.DataColumn.AllowDBNull is false</exception>
        /// <exception cref="System.FormatException">column is not a valid format.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range.</exception>
        public DataRow ReadRow(
            DataTable table,
            bool addToTable)
        {
            // If we can not start a row, return false
            if (!ReadRowStart())
                return null;

            // Create the data row
            DataRow row = table.NewRow();

            // Get the columns for the table
            DataColumnCollection columns = table.Columns;
            int count = columns.Count;

            // For each column in the table...
            for (int index = 0; isInRow && index < count; index++)
            {
                // Get the column
                DataColumn column = columns[index];

                // Read the column value
                row[index] = ReadColumn(column.DataType);
            } // For each column in the table...

            // End the row
            ReadRowEnd();

            // Add the row to the table
            if (addToTable)
                table.Rows.Add(row);

            // Return the row
            return row;
        }

        /// <summary>
        /// Start a new row.
        /// </summary>
        /// <returns>True, if a row was started; otherwise, false.</returns>
        /// <exception cref="System.InvalidOperationException">CSV reader is already in a row.</exception>
        public bool ReadRowStart()
        {
            // If we are already in a row, throw an exception
            if (isInRow)
                throw new InvalidOperationException("CSV reader is already in a row.");

            // Start a row
            isInRow = !isEndOfStream && BaseReader.Peek() >= 0;
            isFirstCharInLine = true;
            return isInRow;
        }

        /// <summary>
        /// Read until the end of the current row is reached.
        /// </summary>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        public void ReadRowEnd()
        {
            // Loop until we reach the end of the current row...
            while (isInRow)
                ReadString(true);

            // Raise the RowEnd event
            OnRowEnd(EventArgs.Empty);
        }

        /// <summary>
        /// Read until the end of the current row is reached.
        /// </summary>
        /// <param name="disableEvent">A value indicating if the RowEnd event is disabled.</param>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        public void ReadRowEnd(
            bool disableEvent)
        {
            // Loop until we reach the end of the current row...
            while (isInRow)
                ReadString(true);

            // If the RowEnd event is not disabled, raise it
            if (!disableEvent)
                OnRowEnd(EventArgs.Empty);
        }

        /// <summary>
        /// Skip a row.
        /// </summary>
        /// <returns>True, if a row was skipped; otherwise, false.</returns>
        public bool SkipRow()
        {
            // Try to start a row
            bool start = ReadRowStart();

            // If we started a row, read to the end of the row
            if (start)
                ReadRowEnd();

            // Indicate if a row was started
            return start;
        }

        #endregion // Public methods (rows)

        #region Public methods (tables)

        /// <summary>
        /// Creates a data table.
        /// </summary>
        /// <param name="hasHeader">A value indicating if the table has a header.</param>
        /// <returns>The table that is created.</returns>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        public DataTable CreateTable(
            bool hasHeader)
        {
            return CreateTable(hasHeader, null, null);
        }

        /// <summary>
        /// Creates a data table.
        /// </summary>
        /// <param name="hasHeader">A value indicating if the table has a header.</param>
        /// <param name="tableName">The name to give the table. If tableName is null or an empty string, a default name is given when added to the System.Data.DataTableCollection.</param>
        /// <returns>The table that is created.</returns>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        public DataTable CreateTable(
            bool hasHeader,
            string tableName)
        {
            return CreateTable(hasHeader, tableName, null);
        }

        /// <summary>
        /// Creates a data table.
        /// </summary>
        /// <param name="hasHeader">A value indicating if the table has a header.</param>
        /// <param name="tableName">The name to give the table. If tableName is null or an empty string, a default name is given when added to the System.Data.DataTableCollection.</param>
        /// <param name="tableNamespace">The namespace for the XML representation of the data stored in the DataTable.</param>
        /// <returns>The table that is created.</returns>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        public DataTable CreateTable(
            bool hasHeader,
            string tableName,
            string tableNamespace)
        {
            // Assume table table creation fails
            DataTable table = null;

            try
            {
                // Try to create the table...
                // Create the table
                table = new DataTable(tableName, tableNamespace);

                // Generate column names from header (or first row)
                GenerateColumns(table, hasHeader);

                // Return the table
                return table;
            } // Try to create the table...
            catch
            {
                // Catch any exceptions...
                // If necessary, dispose of the table
                if (table == null)
                    table.Dispose();

                // Re-throw the exception
                throw;
            } // Catch any exceptions...
        }

        /// <summary>
        /// Read all of the rows and add them to the specified table.
        /// </summary>
        /// <param name="table">The table to which the rows are added.</param>
        /// <exception cref="System.ArgumentException">type is not supported.</exception>
        /// <exception cref="System.Data.ConstraintException">The addition invalidates a constraint.</exception>
        /// <exception cref="System.Data.NoNullAllowedException">The addition tries to put a null in a System.Data.DataColumn where System.Data.DataColumn.AllowDBNull is false</exception>
        /// <exception cref="System.FormatException">column is not a valid format.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range.</exception>
        public void ReadTable(
            DataTable table)
        {
            // Reset the cancellation pending flag
            CancellationPending = false;

            // Loop to read all of the rows...
            while (!CancellationPending && ReadRow(table, true) != null) ;
        }

        /// <summary>
        /// Read (and create) a data table.
        /// </summary>
        /// <param name="hasHeader">A value indicating if the table has a header.</param>
        /// <returns>The data table.</returns>
        /// <exception cref="System.ArgumentException">type is not supported.</exception>
        /// <exception cref="System.Data.ConstraintException">The addition invalidates a constraint.</exception>
        /// <exception cref="System.Data.NoNullAllowedException">The addition tries to put a null in a System.Data.DataColumn where System.Data.DataColumn.AllowDBNull is false</exception>
        /// <exception cref="System.FormatException">column is not a valid format.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.NotSupportedException">The stream does not support seeking, such as if the stream is constructed from a pipe or console output.</exception>
        /// <exception cref="System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range.</exception>
        public DataTable ReadTable(
            bool hasHeader)
        {
            return ReadTable(hasHeader, null, null);
        }

        /// <summary>
        /// Read (and create) a data table.
        /// </summary>
        /// <param name="hasHeader">A value indicating if the table has a header.</param>
        /// <param name="tableName">The name to give the table. If tableName is null or an empty string, a default name is given when added to the System.Data.DataTableCollection.</param>
        /// <returns>The data table.</returns>
        /// <exception cref="System.ArgumentException">type is not supported.</exception>
        /// <exception cref="System.Data.ConstraintException">The addition invalidates a constraint.</exception>
        /// <exception cref="System.Data.NoNullAllowedException">The addition tries to put a null in a System.Data.DataColumn where System.Data.DataColumn.AllowDBNull is false</exception>
        /// <exception cref="System.FormatException">column is not a valid format.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.NotSupportedException">The stream does not support seeking, such as if the stream is constructed from a pipe or console output.</exception>
        /// <exception cref="System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range.</exception>
        public DataTable ReadTable(
            bool hasHeader,
            string tableName)
        {
            return ReadTable(hasHeader, tableName, null);
        }

        /// <summary>
        /// Read (and create) a data table.
        /// </summary>
        /// <param name="hasHeader">A value indicating if the table has a header.</param>
        /// <param name="tableName">The name to give the table. If tableName is null or an empty string, a default name is given when added to the System.Data.DataTableCollection.</param>
        /// <param name="tableNamespace">The namespace for the XML representation of the data stored in the DataTable.</param>
        /// <returns>The data table.</returns>
        /// <exception cref="System.ArgumentException">type is not supported.</exception>
        /// <exception cref="System.Data.ConstraintException">The addition invalidates a constraint.</exception>
        /// <exception cref="System.Data.NoNullAllowedException">The addition tries to put a null in a System.Data.DataColumn where System.Data.DataColumn.AllowDBNull is false</exception>
        /// <exception cref="System.FormatException">column is not a valid format.</exception>
        /// <exception cref="System.InvalidOperationException">CSV reader is not in a row.</exception>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.NotSupportedException">The stream does not support seeking, such as if the stream is constructed from a pipe or console output.</exception>
        /// <exception cref="System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
        /// <exception cref="System.OverflowException">column contains a number that is out of range.</exception>
        public DataTable ReadTable(
            bool hasHeader,
            string tableName,
            string tableNamespace)
        {
            // Assume we can not create the table
            DataTable table = null;

            try
            {
                // Try to create the table...
                // Create and read the table
                table = CreateTable(hasHeader, tableName, tableNamespace);

                // Read the table
                ReadTable(table);

                // Return the result
                return table;
            } // Try to create the table...
            catch
            {
                // If we need to dispose of the table, do so
                if (table != null)
                    table.Dispose();

                // Re-throw the exception
                throw;
            }
        }

        /// <summary>
        /// Generate the columns for a table.
        /// </summary>
        /// <param name="table">The table for which the columns are generated.</param>
        /// <param name="hasHeader">A value indicating if the file contains a header column.</param>
        /// <exception cref="System.IO.IOException">An I/O exception occurs.</exception>
        /// <exception cref="System.NotSupportedException">The stream does not support seeking, such as if the stream is constructed from a pipe or console output.</exception>
        /// <exception cref="System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
        protected virtual void GenerateColumns(
            DataTable table,
            bool hasHeader)
        {
            // If we have no rows, just return
            if (!ReadRowStart())
                return;

            // Get the collection of columns
            DataColumnCollection columns = table.Columns;

            // If we have a header row...
            if (hasHeader)
            {
                // Loop to add the column names...
                while (isInRow)
                    columns.Add(NameSpace.CreateName(NameSpace.CleanName(ReadString())));
            } // If we have a header row...
            else
            {
                // ELSE: If we have a header row...
                // Loop to add the columns (based on generated names)...
                while (isInRow)
                {
                    // Skip the column
                    ReadString(true);

                    // Add a column (with a generated name)
                    columns.Add(NameSpace.CreateName());
                } // Loop to add the columns (based on generated names)...
            } // ELSE: If we have a header row...

            // Read the end of the row
            ReadRowEnd(!hasHeader);

            // If no header row, rewind to start
            if (!hasHeader)
                if (BaseReader is StreamReader)
                {
                    // Get the stream reader
                    var streamReader = (StreamReader) BaseReader;

                    // Get the base stream
                    streamReader.BaseStream.Seek(0L, SeekOrigin.Begin);

                    // Discard buffer so we start reading at the beginning
                    streamReader.DiscardBufferedData();
                }
                else
                    throw new NotSupportedException("TextReader does not supported seeking.");
        }

        #endregion // Public methods (tables)

        #endregion // Public methods

        #region Protected methods

        /// <summary>
        /// Update option variables based on value of options.
        /// </summary>
        protected override void UpdateOptions()
        {
            isIgnoreCr = (options & CsvOption.IgnoreCr) != 0;
            isEquals = (options & CsvOption.Equals) != 0;
            isTrim = (options & CsvOption.Trim) != 0;
            base.UpdateOptions();
        }

        #endregion // Protected methods

        #region Private methods

        /// <summary>
        /// Check for the end of the row.
        /// </summary>
        /// <param name="nextChar">The character to check.</param>
        /// <returns>True, if at end of row; otherwise, false.</returns>
        private bool isEndRow(
            char nextChar)
        {
            bool result;

            // If end of row preceeded by CR...
            if (isIgnoreCr && nextChar == '\r' && BaseReader.Peek() == EndRowChar)
            {
                // Skip end of row and remember it
                BaseReader.Read();
                result = true;
            } // If end of row preceeded by CR...
            else
                result = nextChar == EndRowChar;

            // If end of row, exit the row
            if (result)
                isInRow = false;

            // Return the result
            return result;
        }

        #endregion // Private methods

        /// <summary>
        /// Dispose of resources associated with this instance.
        /// </summary>
        public override void Dispose()
        {
            // If nothing to dispose, just return
            if (!isLocalReader || BaseReader == null)
                return;

            // Dispose of the text reader
            try
            {
                BaseReader.Dispose();
            }
            finally
            {
                BaseReader = null;
            }
        }
    }
}